=====================

Winsock API Tutorial Part 4

Written By

Mohamed Fayed

Thensync@hotmail.com

=====================

 

 

برمجة الخادم ((Server

 

Binding:

 

في الدرسين السابقين تعرفنا علي كيفية استدعاء خدمة أل Winsock وإنشاء مقبس ومعرفة قيمة مقبضه.

الآن سنتعرف على كيفية استخدام هذا المقبس في ربطه على منفذ Port  معين ووضع المنفذ في حالة استماع – تماما مثل أداه MsWinsck.ocx- وانتظار قدوم أي إشعار بالاتصال من الطرف الأخر

 

ولكي  نقوم بربط المقبس على عنوان IP و منفذ Prot  نستخدم الدالة bind()

وتجد التصريح لها هكذا

Public Declare Function bind Lib "ws2_32.dll" ( _
   ByVal s As Long, ByRef Name As sockaddr_in, ByRef namelen As Long) As Long

 

حيث تمثل الثلاث توابع الأتى :

  S            قيمة المقبض الناتج عن استخدام دالة Socket()

sockaddr_in          بنية يتم ملؤها بمعلومات مثل المنفذ المستخدم والعنوان IP

namelen                  حجم البنية sockaddr_in .

 

         

و أل Syntax لدالة bind() :

lRetVal = bind(LlistenSocket, sockaddr_in, Len(sockaddr_in))

لو لاحظت فى تصريحات  mdlWinsock

Public Type sockaddr_in

    sin_family As Integer

    sin_port As Integer

    sin_addr As Long

End Type

هذه البنية sockaddr_in  تتكون من :

sin_family   : تمثل عائلة العنوان ونضع بها القيمة AF_INET لأننا نستخدم العائلة  Ipv4.
 :     sin_port
رقم المنفذ المراد الاستماع عليه.
  :   sin_addr  تحديد
عنوان IP للطرف الآخر، واستخدمنا هنا العنوان INADDR_ANY  فهو يجعل الخادم يقبل أي                      

                    اتصال من أي عنوان IP..فهو ليس محدد بقبول عنوان معين.   

Public Const INADDR_ANY = &H0  'From mdlWinsock Declrationss

Public Const AF_INET = 2          'UDP, TCP,..

Public Const IPPROTO_TCP = 6      'TCP Protocol

Public Const SOCK_STREAM = 1

 


ويتم ملء بنية sockaddr_in هكذا:

sockaddr_in.sin_family = AF_INET         'IP Protocol Family

sockaddr_in.sin_addr = INADDR_ANY        'accecpt any incoming connection requests

sockaddr_in.sin_port = htons(CLng(5000(( 'on port 5000

 

ولاحظ أننا استخدمنا دالة htons - أو ((Host byte To NetWork byte order For Short Numbers وأل Short Numbers ستحمل قيم نوعها Integer وهي أرقام المنافذ من صفر إلى 65535 -ويمكنك مراجعة التصريح  الذي وضعناه للدالة - .وذلك لاختلاف الشبكات فتم الاتفاق على طريقة واحدة لتمثيل العناوين .فأي عنوان IP أو Port يمرر عبر مقابس الويندوز في بنية sockaddr_in  - ما عدا sin_family طبعا - لازم أن يكون من نوع الشبكة((Must be In Network  Byte Order. فأي عنوان IP وليكن 127.0.0.1 لابد من أن يكون ممثل بالبايت وهذا ما تقوم الدالة بعمله .

طبعا سنستخدم دوال تساعدنا علي هذا التمثيل مثل htons()  لأرقام المنافذ ولها دالة للتحويل العكسي - للعلم فقط- تدعى  ntohs() .وسنستخدم  عندما نتصل بالخادم دالة inet_addr() لتحويل رقم IP من عنوان مفصول بـ (.) إلى عنوان 32 bit من نوع الشبكة.

ويبقى أن نربط  معلومات بنية sockaddr_in بالمقبس الذى انشئناه وذلك عن طريق دالة bind

 

lRetVal = bind(LlistenSocket, sockaddr_in, Len(sockaddr_in))

إذا حدث خطأ فسيعود المتغير lRetVal ب SOCKET_ERROR  وأيضا قيمتها ((-1 .

Public Const SOCKET_ERROR = -1    'From mdlWinsock Declrations

___________________________________________________________________________

هناك خطأ شائع وهو WSAEADDRINUSE وقيمتها 10048وهو يفيد بأن المنفذ Port في حالة استخدام من قبل برنامج أخر .

ولكي تتغلب على هذه المشكلة ببساطة , قم بتغيير رقم المنفذ.

Public Const WSAEADDRINUSE = 10048

 

Listening:

 

الآن نستطيع أن نضع الخادم في حالة استماع بواسطة الدالة Listen()

والتصريح لها هو

Public Declare Function listen Lib "ws2_32.dll"  (ByVal s As Long, ByVal backlog As Long) As Long

 

ويوجد معاملين للدالة هما:

S          : وهو مقبض المقبس.

Backlog   :وهو متغير رقمي يخزن به أقصى عدد لطلبات الاتصال في صف انتظار الخادم.  

 

وال Syntax للدالة هو :

lRetVal = listen(LlistenSocket, 1)

 

Accepting Connection Requests :

 

بعدما وضعنا الخادم في حالة استماع لأي اتصال قادم يبقى أن نوافق على طلب الاتصال

وذلك باستخدام الدالة Accept()

Public Declare Function accept Lib "ws2_32.dll" (ByVal s As Long, addr As sockaddr_in, namelen As Long) As Long

 

 

الآن سنكتب تطبيقServer  يبين كيفية إنشاء المقبس وكيفية ربطه مع منفذ وكيفية وضعه في حالة استماع و الموافقة على أي اتصال قادم وكيفية إرسال واستقبال بيانات .

 

سيتكون المشروع من:

1-     فورم تدعى frmserver

2-     وحدة نمطية تدعى mdlWinsock وسنضع بها تعريفات مكتبة الوينسوك زائد كود إلقاء خطاف رسائل المقبس Hook

3-     وحدة نمطية أخري أخرى للتصنيف الفرعي تدعى mdlSubClass .

 

وهذا هو ملخص ما سنفعله :

أولا :

        في حدث أل Form_Load لكلا من ألClient  و أل Server سنلقى بخطاف Hook وذلك لمراقبة            

         رسائل المقبس ( Socket).

ثانيا :

       سننشأ إجراءان فرعيان لكلا من (تهيئة وتحميل مكتبة الوينسوك) وسيكون باسم  

        WinsockInitialize() فى الوحدة النمطية mdlWinsock

       وأجراء أخر لإنشاء المقبس وعمل bind له ثم وضع المقبس فى حالة استماع .وليكن الأجراء 

        باسم WinsockListen()

ثالثا :

       الخطاف hook  الذي وضعناه سيكون عنوان تصنيفه الفرعي SubClass هو WinsockProc()           

       وفى هذا التصنيف الفرعي.

       سنتعرف على  الرسائل التي يستقبلها المقبس .وكيفية معالجة كل رسالة والرد عليها .

 

سيتكون مشروعنا من الأتى

الأداة

الأسم

خواص

CmdSend

_

txtChat

MultiLine=True

txtSend

_

lblstatus

_

 

 

ويكون شكل المشروع كالتالى

 

 

 

نبدأ بكتابة كود FrmServer :

 

 

 

Private Sub Form_Load()

 HookForm frmServer.hwnd          'إلقاء خطاف للرسائل

  If WinsockInitialize = True Then   

  ' قمنا بتحميل المكتبة عن طريق إجراء فرعى سنكتبه وحدة نمطية

    WinsockListen

  '  وبأجراء فرعى أخر قمنا بإنشاء مقبس وربطه ووضعه فى حالة استماع

  End If

End Sub

______________________

 

Private Sub Form_Unload(Cancel As Integer)

UnHookForm frmServer.hwnd         'إزالة الخطاف  

If LlistenSocket <> INVALID_SOCKET Then closesocket LlistenSocket

                     ' الكود السابق خاص بغلق المقبس اذا كان مفتوح

WSACleanup           '   ثم إلغاء تحميل المكتبة

End Sub

'______________________

 

Private Sub cmdSend_Click()

Dim Message As String

If Len(txtSend.Text) = 0 Then Exit Sub

Message = "Reply" & "Server : " & txtSend.Text & vbCrLf

lRetVal = send(LlistenClient, ByVal Message, Len(Message), 0)

If lRetVal = SOCKET_ERROR Then

   lblStatus.Caption = "You Are Not Connected"

   Exit Sub

End If

txtChat.Text = txtChat & "Server : " & txtSend.Text & vbCrLf

txtSend.Text = ""

End Sub

في كود ارسال الرسالة إلى العميل Client استخدمنا الدالة Send()

 Declare Function send Lib "ws2_32.dll" (ByVal s As Long, buf As Any, _
     ByVal 
buflen As Long,ByVal flags As Long) As Long 

والدالة لها أربع وسيطات كما ترى :

 

   S     :  القيمة المعادة عند قبول الاتصال والتي تحدد لنا الطرف الأخر.

Buf     :مخزن data للبيانات - (بالبايت) - والتي سترسل إلى الطرف الأخر.

Buflen :حجم البيانات .

Flags  :تابع اختياري ولن نستخدمه وسنضع القيمة له بصفر ..وهو يتحكم في طريقة نقل البيانات.

 

وكود الإرسال كان هكذا :

send(LlistenClient, ByVal data, Len(data), 0)

 

ولتوضيح ما هو LlistenClient ينبغي أن نلقى نظرة على دالة Accept() والتي استقبلنا بها الاتصال القادم من الطرف الأخر

 

LlistenClient = accept(wParam, ClientInternetAddress, ByVal AddressLen)

 

فعندما يتم قبول الاتصال تعود دالة Accept() بقيمة رقمية نخزنها فى متغير من نوع  Long هو LlistenClient ,تلك القيمة نستطيع منها تحديد الجهاز المتصل مع الخادم Server, والذي يتم إرسال البيانات إلي الخادم منه, وبالتالي عند تصميم برنامج MultiClients مثل برنامج Net Café Controller  يمكننا تحديد كل جهاز متصل مع الخادم Server عن طريق تلك القيمة, ونستطيع إرسال أي رسالة إلي أي جهاز متصل أيضا عن طريق تلك القيمة.

 

ولازم عندما نستخدم دالة Send() أن نحدد له مقبض (handle) الطرف الأخر الذى نتعامل معه وسيلي شرح دالة Accept .

 

إذا لم يحدث أي خطأ أثناء إرسال البيانات فتعود الدالة بعدد البايتات ((Bytes التي أرسلت وإذا حدث خطأ فتعود ب SOCKET_ERROR وقيمتها (-1) .

 

هذا هو كل ما سنكتبه في فورم FrmServer   وننطلق الآن إلى كتابة اكواد الوحدة النمطية mdlWinsock

سنستخدم طبعا تصريحات ألWinsock API  ونضيف عليها التصريحات الآتية والخاصة بعمل hook وSubclass:

 

Public lpWinsock as long

Public Const GWL_WNDPROC = (-4)

Public Declare Function SetWindowLong Lib "user32" Alias "SetWindowLongA" (ByVal _

      Hwnd As Long,ByVal nIndex  As Long, ByVal dwNewLong As Long) As Long

Declare Function CallWindowProc Lib "user32" Alias "CallWindowProcA" (ByVal lpPrevWndFunc As Long, ByVal & _

      hwnd As Long, ByVal MSG As Long, ByVal wParam As Long, ByVal lParam As Long) As Long

 

وستكون الاكواد المضافة على الوحدة النمطية خاصة بإجراءات فرعية لتحميل مكتبة الوينسوك وإنشاء مقبس ووضعه في حالة استماع .وعمل الإجراءات الفرعية لخطاف الرسائل Hook و Unhook

 

 

 

 

Public Function WinsockInitialize() As Boolean

Dim lRetVal As Long

Dim WSAData As WSADataType

lRetVal = WSAStartup(&H202, WSAData)

If lRetVal <> 0 Then

  frmServer.lblStatus = "Can't Load winsock Service"

      WinsockCleanup

   Exit Function

End If

WinsockInitialize = True

' هذه القيمة السابقة لن تظهر إلا لو نجحت الدالة في تحميل المكتبة وبالتالي يمكننا

' Form_Load استخدامها كشرط لإنشاء مقبس كما فعلنا في حدث

 

End Function

 

Public Sub HookForm(hwnd As Long)

lpWinsock = SetWindowLong(hwnd, GWL_WNDPROC, AddressOf Winsockmain)

' Winsockmain تثبيت الخطاف لتصنيف فرعى

End Sub

 

Public Sub UnHookForm(hwnd As Long)

SetWindowLong hwnd, GWL_WNDPROC, lpWinsock  'ازالة الخطاف

End Sub

 

Public Sub WinsockListen()

Dim InternetAdrr As sockaddr_in

 

On Error Resume Next

LlistenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)

If listenSocket = INVALID_SOCKET Then

      frmServer.lblStatus.Caption = "Error While Creating Socket"

   Exit Sub

End If

 

InternetAdrr.sin_family = AF_INET

InternetAdrr.sin_port = htons(CLng("2010"))

InternetAdrr.sin_addr = INADDR_ANY

 

lRetVal = bind(Winsock, InternetAdrr, Len(InternetAdrr))

If lRetVal = SOCKET_ERROR Then

   closesocket LlistenSocet

   frmServer.lblStatus.Caption = "Error While binding Socket"

   Exit Sub

End If

 

WSAAsyncSelect LlistenSocket, frmServer.hwnd, WM_SOCKET, FD_ACCEPT

'سيلى شرح الدالة السابقة بالتفصيل

 

lRetVal = listen(LlistenSocket, 1)

If lRetVal = SOCKET_ERROR Then

   closesocket LlistenSocket

   frmServer.lblStatus.Caption = "Error While Listening on port 2010"

   Exit Sub

End If

frmServer.lblStatus.Caption = "Ready To Accept Any Request"

End Sub

 

الدالة WSAAsyncSelect تمكننا من استقبال أحداث المقبس Socket Events من خلال الرسائل الواردة إليه:

 

Declare Function WSAAsyncSelect Lib "wsock32.dll" (ByVal s As Long, ByVal hwnd As 

                         Long, ByVal wMsg As Long,  ByVal lEvent As Long) As Long

 

 ولها أربع وسيطات :

 

 :     S  المقبس الذي ستستقبل الدالة أحداثه, وهو طبعا LlistenSocket

Hwnd : مقبض النافذة التي سيتم إرسال أحداث المقبس إليها وهى frmServer ,ولا تنسي أننا

         قمنا بعمل hook على هذه النافذة وسنستقبل الرسائل التي تصلها ونقدر نحدد اى رسالة 

        خاصة بحدث للمقبس عن طريق الوسيط الثالث wMsg

 

wMsg  : وهى الرسالة التي سيستقبلها التصنيف الفرعى Winsockmain الموجود في الوحدة 

         النمطية mdlSubClass وهي قيمة ثابتةWM_SOCKET وتجد تلك القيمة في تصريحات

          الوحدة النمطية mdlWinsock.وعندما يستقبل التصنيف الفرعي هذه الرسالة

        WM_SOCKET سيعرف أن هناك حدث للمقبس ويتم تحديد الحدث بجملة Select Case .

 

lEvent : من خلال هذا التابع يمكن تحديد الأحداث التي نريد مراقبتها وقد مررنا لها الحدث

          FD_ACCEPT وهو حدث خاص باستشعار قدوم طلب اتصال من الطرف الأخر ..وبعدما يحدث

         الاتصال , سنعيد تمرير تلك الدالة مره أخرى لكن لمراقبة هذه المرة أحداث أخرى للمقبس

         مثل قدوم بيانات FD_READ أو حدث لإغلاق المقبس FD_CLOSE من الطرف الأخر ..

 

  تعود الدالة بالقيمة صفر إذا نجحت في استقبال حدث وإلا ستعود ب SOCKET_ERROR                 

___________________________________________________________________________

الآن ندخل في كتابة اكواد الوحدة النمطية mdlSubClass حتى تعرف ( هو فيه آية ؟)

لأن هذه الوحدة النمطية هي التي ستفسر لك كل شيء عن أحداث المقبس .وكيفية تنظيم البيانات بعد إرسالها إليك من الطرف الأخر .

واكواد الوحدة النمطية بسيطة للغاية – لكن مهم جدا أنك تكون فاهم باب

 Hooking and SubClassing - :

 

Public LlistenClient as long

Public Function Winsockmain(ByVal hwnd As Long, ByVal uMsg As Long, _

                         ByVal wParam As Long, ByVal lParam As Long) As Long

 If uMsg = WM_SOCKET Then

 Dim ClientInternetAddress As sockaddr_in, AddressLen As Long

 Dim ReadBuffer(1024) As Byte

 Dim Data As String 

 On Error Resume Next

 Select Case lParam

  Case FD_ACCEPT

       LlistenClient = accept(wParam, ClientInternetAddress, ByVal AddressLen)

       WSAAsyncSelect LlistenClient, hwnd, WM_SOCKET, FD_READ Or FD_CLOSE

       frmServer.lblStatus.Caption = "Connected"

       If LlistenClient = SOCKET_ERROR Then

          frmServer.lblStatus.Caption = "Disconnected"

       End If

  Case FD_READ

       lRetVal = recv(wParam, ReadBuffer(0), 1024, 0)

       If lRetVal > 0 Then

          Data = Left(StrConv(ReadBuffer, vbUnicode), lRetVal)

 

         If Left(Data, 5) = "Reply" Then

          Data = Right(Data, Len(Data) - 5) 

          frmServer.txtChat.Text = frmServer.txtChat.Text & Data

         End If

       End If

 Case FD_CLOSE

       frmServer.lblStatus.Caption = "The Remote Machine Disconnected"

 End Select

End If

 

 Winsockmain = CallWindowProc(lpWinsock, hwnd, uMsg, wParam, lParam)

End Function

 

لقد قمنا بتحديد الرسالة التي ستحمل أحداث المقبس عندما استخدمنا دالة WSASyncSelect() وحدننا لها القيمة الثابتة WM_SOCKET وكتبنا شرط إذا كانت الرسائل المرسلة للنافذة خاصة بالمقبس فيعمل تحدد حالات Select case للأحداث الخاصة بالمقبس:

If uMsg = WM_SOCKET Then

  Select Case lParam

 

     '--------

     '--------

 

   End Select

End If

 

نلاحظ أن قيمة lParam تحمل نوع الرسالة (هل هيFD_ACCEPT ,FD_READ أم (FD_Close

أم قيمة  wParam  فتحمل مقبض Handle للطرف الأتي من الرسالة, ولاحظ أننا - (بعد الموافقة على الاتصال( -  سنستخدم القيمة المعادة من الدالة Accept() وهى LlistenClient لتمريرها للدالة WSASyncSelect() مرة أخرى لاستقبال أحداث المقبس الأخرى الخاصة بالجهاز الموجود عليه برنامج العميل Client الذي استقبلنا منه الاتصال, وتمت مراقبة الحدث FD_Accept بنجاح.

 

شرح بسيط لأحداث المقبس :

 

FD_ACCEPT  : يتم إرسال هذا الحدث إلى مقبس برنامج الخادم Server عندما يطلب الطرف الأخر  Client الاتصال 

                     ببرنامجنا وسيتم الموافقة على الاتصال القادم باستخدام دالة Accept() وهذه

                    الدالة تشبه إلى حد كبير دالة bind().

Declare Function accept Lib "ws2_32.dll" (ByVal s As Long, ByRef addr As sockaddr_in , ByRef addrlen As Long) As Long

ولها ثلاث معاملات :

S            :  قيمة مقبض تمثل الطرف الأخر القادم منه الاتصال.

Addr        : بنية من نوع sockaddr_in يتم ملؤها ببيانات العنوان للطرف الأخر.

 Addrlen   : حجم البنية .

 

 

FD_READ  : يتم إرسال هذا الحدث إلى مقبس الخادم ..عندما يكون هناك بيانات قادمة من الطرف الأخر ونستقبل تلك البيانات المرسلة بالدالةRecv()  وهى كبيرة الشبة بالدالة  Send() .

 

Declare Function recv Lib "ws2_32.dll" (ByVal s As Long,ByRef buf As Any, _
   ByVal 
buflen As Long,ByVal flags As Long) As Long 

والمعاملات هي :

 

  :       S   قيمة مقبض الطرف الأخر .

buf      : مخزن معلوماتbuffer  data .. ويخزن به المعلومات المرسلة  .

Buflen  : حجم هذا المخزن بالبايت .

 

 

وقد قمنا بتعرف مصفوفة ReadBuffer(1024) ليستقبل بايتات بحجم 1024 أي أن الخادم سيستقبل بايتات لفقط واحد كيلو بايت في المرة الواحدة وإذا زادت عدد البايتات عن هذا الرقم فيجب زيادة ذلك الحجم ليصبح ReadBuffer(8192) أو نقوم بالتأكد من وصول البيانات كاملة للطرف الأخر وذلك من خلال عمل Loop لتقسيم البيانات وبحجم واحد كيلو في المرة الواحدة.

 

فمثلاًً إذا قمنا بتصميم مدير الملفات ((File Manager  لأي غرض سواء لبرنامج هاك أو لأي برنامج خدمي وطلبنا رؤية أسماء مجلدات وملفات فإذا حاولت أن تستعرض محتوى مجلد وليكن مجلد System  بحجم استقبال بيانات يصل لواحد كيلو (1024) فلن تستطيع سوى استعراض 120 مجلد و ملف على الأكثر مع أنك ستجد أن بمجلد System أكثر من 1000 ملف.وبالتالي يجب زيادة حجم المخزن Buffer حتى يقوم باحتواء جميع البيانات المرسلة.

 

ولم يتبقى سوي تحويل تلك البايتات التي استقبلناها داخل المصفوفة المعرفة كـ Byte وهي ReadBuffer(1024)  إلى قيم Unicode  حتى نستطيع قراءتها وذلك عن طريق استخدام دالة StrConv() (String Convert)  هكذا     

 lRetVal = recv(wParam, ReadBuffer(0), 1024, 0)

  If lRetVal > 0 Then

   Data = Left(StrConv(ReadBuffer, vbUnicode), lRetVal)

   '------

   '------

  End If

 

 

 

لاحظ أن الدالة recv() عندما تستقبل بيانات فتعود  lRetVal بعدد البايتات المستقبلة وهكذا نستطيع أن نخزن القيم المحولة في المتغير Data  باستخدام الدالةLeft()  وطول السلسلة سيكون lRetVal وهو عدد البايتات المستقبلة ..

 

لاحظ أيضا أن الرسالة القادمة من الطرف الأخر ستكون هكذا

data=ReplayHi back

وهنا سنضطر إلى قطع الكلمة replay بدوال التعامل مع النصوص :

If Left(Data, 5) = "Reply" Then

data = Right(Data, Len(Data) – 5)

 If Left(Data, 5) = "Reply" Then

   Data = Right(Data, Len(Data) - 5

   frmServer.txtChat.Text = frmServer.txtChat.Text & VbCrlf & Data

  End If

الآن أصبح المتغير data يحمل القيمة Hi back ومنها نظهر تلك القيمة في txtchat  

 

 

 

FD_CLOSE  : يستقبل المقبس هذا الحدث إذا تم إغلاق الاتصال من الطرف الأخر ..ويمكننا في

                    هذه الحالة إظهار رسالة تبين أنه لم يعد هناك اتصال مع الطرف الأخر .

 

 

طبعا في النهاية لا تنسى أن تمرر رسائل المقبس بشكل طبيعيي إلى النافذة الرئيسية frmserver التي قمنا بعمل Hook عليها

لأننا إذا  لم نمرر تلك الرسائل كما عرفنا من الجزء الخاص بـ Hooks and Subclassing  فلن يستقبل الفورمFrmServer  أي حدث للمقبض سواء كان بيانات الشات (Chat) أو حالة الاتصال وهكذا .

 

Winsockmain = CallWindowProc(lpWinsock, hwnd, uMsg, wParam, lParam)

وهكذا نكون قد انتهينا من برمجة الخادم .ويبقى أن نبرمج ألClient   ..

 

 

 

=====================

Winsock API Tutorial Part 4

Written By

Mohamed Fayed

Thensync@hotmail.com

=====================

More Info Visit The source of Network Programming

www.vbip.com